﻿/***************************************************************************
 * 
 *  Copyright 2008 Anthony Tambrin
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 * 
 *  
 *  @page 				AutoScrollPanel.as
 *  @version			0.1
 *  @description		Automatic scrolling panel component class. 
 * 						Features include:
 * 						1) Automatic scrolling
 * 						2) Snap to object
 * 
 ***************************************************************************/

package com.ui {
	
	/********************************************************
	 * Imports
	 ********************************************************/
	import flash.display.MovieClip;
	import flash.events.MouseEvent;
	import flash.events.Event;
	import flash.display.DisplayObject;
	import flash.geom.Rectangle;
	import flash.display.Stage;
	
	public class AutoScrollPanel extends MovieClip {
		
		/********************************************************
		 * Public Constants
		 ********************************************************/
		private static const MAX_ZOOM:Number = 2;
		private static const MIN_ZOOM:Number = 0.4;
		
		/********************************************************
		 * Private Constants
		 ********************************************************/
		private static const DEFAULT_CONTENT_MOVEMENT_DELAY:Number = 0.1;
		private static const DEFAULT_CONTENT_ZOOM_DELAY:Number = 0.1;
		private static const DEFAULT_ZOOM_FACTOR:Number = 0.1;
		private static const DEFAULT_SNAP:Boolean = false;
		
		/********************************************************
		 * Private Components
		 ********************************************************/
		private var _stage:Stage;
		private var _content:MovieClip; 
		private var _mask:MovieClip;
		
		/********************************************************
		 * Private Members
		 ********************************************************/
		private var _originalContentMovementDelay:Number;
		private var _contentMovementDelay:Number;
		private var _contentZoomDelay:Number;
		private var _isRollOver:Boolean;
		private var _currentFocusObject:DisplayObject;
		private var _focusObjects:Array;
		private var _ratioX:Number;
		private var _ratioY:Number;
		private var _dx:Number;
		private var _dy:Number;
		private var _dw:Number;
		private var _dh:Number;
		private var _dist:Number;
		private var _dist2:Number;
		private var _contentTargetX:Number;
		private var _contentTargetY:Number;
		private var _contentTargetWidth:Number;
		private var _contentTargetHeight:Number;
		private var _localMouseX:Number;
		private var _localMouseY:Number;
		private var _contentHitArea:MovieClip;
		private var _maskBoundary:Rectangle;
		private var _snap:Boolean;
		private var _snapRadius:Number;
		private var _zoomFactor:Number;
		private var _zoomValue:Number;
		private var _zoomWidthDiff:Number;
		private var _zoomHeightDiff:Number;
		private var _originalContentWidth:Number;
		private var _originalContentHeight:Number;
		private var _widthToHeightRatio:Number;
		private var _heightToWidthRatio:Number;
	
		/********************************************************
		 * Constructor
		 ********************************************************/
		public function AutoScrollPanel(p_content:MovieClip, p_mask:MovieClip, p_initObject:Object=null) {
			p_initObject = p_initObject ? p_initObject : new Object();
	  
			_content = p_content;
			_mask = p_mask;
			initComponents();
			initMembers();
			validate();
			
			addEventListener(Event.ENTER_FRAME, onContainerEnterFrame);
			_content.addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
			
			if (_content.stage) {
				mainStage = _content.stage;
			}
		}
	
		/********************************************************
		 * Initialisation Methods
		 ********************************************************/
		private function initMembers():void {
			_isRollOver = false;
			_originalContentMovementDelay = DEFAULT_CONTENT_MOVEMENT_DELAY;
			_contentMovementDelay = _originalContentMovementDelay;
			_contentZoomDelay = DEFAULT_CONTENT_ZOOM_DELAY;
			_focusObjects = new Array();
			_maskBoundary = new Rectangle();
			_snap = DEFAULT_SNAP;
			_snapRadius = 0;
			_zoomFactor = DEFAULT_ZOOM_FACTOR;
			_contentTargetX = 0;
			_contentTargetY = 0;
			_originalContentWidth = _content.content.width;
			_originalContentHeight = _content.content.height;
			_contentTargetWidth = _originalContentWidth;
			_contentTargetHeight = _originalContentHeight;
			_zoomWidthDiff = 0;
			_zoomHeightDiff = 0;
			_zoomValue = 1;
			_widthToHeightRatio = _originalContentWidth/_originalContentHeight;
			_heightToWidthRatio = _originalContentHeight/_originalContentWidth;
		}
		
		private function initComponents():void {
			// align content with mask
			_content.x = _mask.x;
			_content.y = _mask.y;
			
			// create hit area by default for content
			_contentHitArea = new MovieClip();
			with (_contentHitArea.graphics) {
				beginFill(0xFF0000, 0);
				drawRect(0, 0, _mask.width, _mask.height);
				endFill();
			} 
			_content.addChild(_contentHitArea);
			_content.swapChildren(_content.content, _contentHitArea);
		}
		
		private function initStageListeners():void {
			if (!_stage) return;
			_stage.removeEventListener(MouseEvent.MOUSE_WHEEL, onComponentWheelOutside);
			_stage.addEventListener(MouseEvent.MOUSE_WHEEL, onComponentWheelOutside);
		}
		
		/********************************************************
		 * Accessors
		 ********************************************************/
		public function set delay(p_contentMovementDelay:Number):void {
			_originalContentMovementDelay = p_contentMovementDelay;
			_contentMovementDelay = _originalContentMovementDelay;
		}
		public function get delay():Number {
			return _contentMovementDelay;
		}
		
		public function set snap(p_snap:Boolean):void {
			_snap = p_snap;
		}
		public function get snap():Boolean {
			return _snap;
		}
		
		public function set zoomFactor(p_zoomFactor:Number):void {
			_zoomFactor = p_zoomFactor;
		}
		public function get zoomFactor():Number {
			return _zoomFactor;
		}
		
		public function set mainStage(p_stage:Stage):void {
			_stage = p_stage;
			initStageListeners();
		}
		
		/********************************************************
		 * Public Methods
		 ********************************************************/
		public function addFocusObject(p_object:DisplayObject):void {
			_focusObjects.push(p_object);
			p_object.addEventListener(MouseEvent.MOUSE_OVER, onFocusObjectMouseOver);
			p_object.addEventListener(MouseEvent.MOUSE_OUT, onFocusObjectMouseOut);
		}
		
		// (Re-)validates all pre-calculated variables
		public function validate():void {
			_maskBoundary.left = _mask.x;
			_maskBoundary.top = _mask.y;
			_maskBoundary.right = _mask.x + _mask.width;
			_maskBoundary.bottom = _mask.y + _mask.height;
			_ratioX = -((_contentTargetWidth - _contentHitArea.width) / _contentHitArea.width);
			_ratioY = -((_contentTargetHeight - _contentHitArea.height) / _contentHitArea.height);
		}
		
		/********************************************************
		 * Private Methods
		 ********************************************************/	
		
		/********************************************************
		 * Event Handlers
		 ********************************************************/
		private function onAddedToStage(p_event:Event):void {
			if (_content.stage) mainStage = _content.stage;
		} 
		
		private function onContainerEnterFrame(p_event:Event):void {
			// ignore if this component is disabled
			if (!enabled) return;
			
			// Update content position		
			if (mouseX >= _maskBoundary.left && mouseX <= _maskBoundary.right && 
				mouseY >= _maskBoundary.top && mouseY <= _maskBoundary.bottom) { 
				_contentTargetX = (mouseX-_maskBoundary.left) * _ratioX;
				_contentTargetY = (mouseY-_maskBoundary.top) * _ratioY;
			}
			
			if (_snap && _isRollOver) {
				_dx = mouseX - (_currentFocusObject.x * _zoomValue + _content.content.x + _content.x);
				_dy = mouseY - (_currentFocusObject.y * _zoomValue + _content.content.y + _content.y);
				_dist = Math.sqrt(_dx*_dx + _dy*_dy);
				_snapRadius = (_currentFocusObject.width * _contentTargetWidth / _originalContentWidth) * 0.5;
				if (_dist < _snapRadius) {
					_dist = _dist/_snapRadius;
					if (_dist == 0) _dist = 0.1;
					_contentMovementDelay *= _dist;
				}
			}
			
			_dx = (_contentTargetX - _content.content.x) * _contentMovementDelay;
			_dy = (_contentTargetY - _content.content.y) * _contentMovementDelay;
			_content.content.x += _dx;
			_content.content.y += _dy;
			
			_contentMovementDelay = _originalContentMovementDelay;
			
			// Update content dimension
			_dw = (_contentTargetWidth - _content.content.width) * _contentZoomDelay;
			_dh = (_contentTargetHeight - _content.content.height) * _contentZoomDelay;
			_content.content.width += _dw;
			_content.content.height += _dh;
		}
		
		private function onFocusObjectMouseOver(p_event:MouseEvent):void {
			// ignore if this component is disabled
			if (!enabled) return;
			
			_isRollOver = true;
			_currentFocusObject = DisplayObject(p_event.currentTarget);
		}
		
		private function onFocusObjectMouseOut(p_event:MouseEvent):void {
			// ignore if this component is disabled
			if (!enabled) return;
			
			_isRollOver = false;
			_currentFocusObject = null;
		}
		
		private function onComponentWheelOutside(p_event:MouseEvent):void {
			// ignore if this component is disabled
			if (!enabled) return;
			
			// zoom out
			if (p_event.delta < 0) {
				if ((_zoomValue += _zoomFactor) > MAX_ZOOM) _zoomValue = MAX_ZOOM;
				_contentTargetWidth = _originalContentWidth * _zoomValue;
				_contentTargetHeight = _originalContentHeight * _zoomValue;
				if (_contentTargetWidth > _originalContentWidth * 2) {
					_contentTargetWidth = _originalContentWidth * 2;
					_contentTargetHeight = _heightToWidthRatio * _contentTargetWidth;
					_zoomValue = _contentTargetWidth / _originalContentWidth;
				} else if (_contentTargetHeight > _originalContentHeight * 2) {
					_contentTargetHeight = _originalContentHeight * 2;
					_contentTargetWidth = _widthToHeightRatio * _contentTargetHeight;
					_zoomValue = _contentTargetWidth / _originalContentWidth;
				}
				validate();
			} 
			// zoom in
			else {
				if ((_zoomValue -= _zoomFactor) < MIN_ZOOM) _zoomValue = MIN_ZOOM;
				_contentTargetWidth = _originalContentWidth * _zoomValue;
				_contentTargetHeight = _originalContentHeight * _zoomValue;
				if (_contentTargetWidth < _mask.width) {
					_contentTargetWidth = _mask.width;
					_contentTargetHeight = _heightToWidthRatio * _contentTargetWidth;
					_zoomValue = _contentTargetWidth / _originalContentWidth;
				} else if  (_contentTargetHeight < _mask.height) {
					_contentTargetHeight = _mask.height;
					_contentTargetWidth = _widthToHeightRatio * _contentTargetHeight;
					_zoomValue = _contentTargetWidth / _originalContentWidth;
				}
				validate();
			}
		}
	}
}